Relying entirely on GitHub for every private repo means trusting a third party with your source code, your issue history, and your CI configuration — fine for…
Relying entirely on GitHub for every private repo means trusting a third party with your source code, your issue history, and your CI configuration — fine for open source, less ideal for client work, personal projects, or anything you'd rather keep fully under your own control. Gitea is a lightweight, self-hosted Git service with a GitHub-like interface, issues, pull requests, and even basic CI, and it runs comfortably on hardware as small as a Raspberry Pi. This guide sets up a production-ready Gitea instance with HTTPS and SSH access from scratch.
Why Gitea Over GitLab or Gogs
GitLab's self-hosted edition is powerful but heavy — it wants several GB of RAM and a fair amount of ongoing maintenance. Gitea (a community fork of Gogs) trades some of GitLab's enterprise features for a fraction of the resource footprint, while still covering what most individuals and small teams actually need: repos, issues, pull requests, wikis, and webhooks. For a personal git server or a small team, it's usually the better fit.
Prerequisites / What You'll Need
- A Linux server or VPS with Docker and Docker Compose installed
- A domain or subdomain pointed at the server (e.g.,
git.yourdomain.com)
- Basic familiarity with Docker Compose and SSH
- 30–45 minutes
Step 1: Point DNS at Your Server
Create an A record for your chosen subdomain pointing at your server's IP address. Do this first — it needs time to propagate before HTTPS certificate issuance will succeed later.
Step 2: Set Up the Docker Compose Stack
Create a project directory and docker-compose.yml:
services:
gitea:
image: gitea/gitea:latest
restart: unless-stopped
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: ${DB_PASSWORD}
GITEA__server__DOMAIN: git.yourdomain.com
GITEA__server__ROOT_URL: https://git.yourdomain.com/
GITEA__server__SSH_DOMAIN: git.yourdomain.com
volumes:
- gitea_data:/data
expose:
- "3000"
ports:
- "222:22"
depends_on:
- db
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: gitea
volumes:
- db_data:/var/lib/postgresql/data
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- gitea
volumes:
gitea_data:
db_data:
caddy_data:
caddy_config:
Notable choices:
- Postgres instead of SQLite — Gitea supports SQLite for tiny setups, but Postgres handles concurrent access far better and makes backups and future scaling simpler.
ports: "222:22" — SSH is mapped to host port 222, not 22. This avoids colliding with the host machine's own SSH daemon, which is still running on port 22 for your regular server access.
- Caddy for automatic HTTPS — same pattern as other self-hosted setups: zero manual certificate management.
Step 3: Configure Secrets
Create a .env file in the same directory:
DB_PASSWORD=change_this_to_something_strong
Add it to .gitignore if this directory is version-controlled, so credentials never end up committed.
Step 4: Configure Caddy
Create a Caddyfile:
git.yourdomain.com {
reverse_proxy gitea:3000
}
Caddy handles the Let's Encrypt certificate automatically on first launch, as long as DNS has propagated and ports 80/443 are reachable.
Step 5: Launch the Stack
docker compose up -d
Watch the logs to confirm both the database connection and certificate issuance succeed:
docker compose logs -f gitea caddy
Step 6: Complete the Initial Setup
Visit https://git.yourdomain.com. Gitea's setup wizard should appear. Most fields will already be correctly pre-filled from your environment variables — review them, then create your admin account on the same screen.
Tip: Create your admin account during this initial setup screen, not afterward — this is the one moment registration is fully open by default, so do it immediately after launch rather than leaving the instance sitting exposed.
Step 7: Lock Down Registration
Once your admin account exists, restrict who else can sign up. In the Gitea admin panel:
- Go to Site Administration → Configuration
- Under Service, disable Enable Self-Registration (or set
DISABLE_REGISTRATION: true as an environment variable and restart the stack instead, for a setting that survives config resets)
For a personal server, this is the single most important lockdown step — an open registration page on a git server exposed to the internet will eventually get found by something automated.
Step 8: Set Up SSH Access
Since SSH runs on the non-standard port 222, add a host entry to make cloning feel natural. On your local machine, edit ~/.ssh/config:
Host git.yourdomain.com
Port 222
User git
Add your SSH public key in the Gitea web UI under Settings → SSH / GPG Keys, then clone normally:
git clone git@git.yourdomain.com:yourusername/yourrepo.git
The custom port in your SSH config means you don't need to remember to append -p 222 to every command.
Step 9: Create Your First Repository
In the web UI, click New Repository, give it a name, and choose whether to initialize with a README. Push an existing local project to it:
git remote add origin git@git.yourdomain.com:yourusername/yourrepo.git
git push -u origin main
Step 10: Set Up Basic CI with Gitea Actions (Optional)
Gitea supports a GitHub Actions-compatible workflow syntax via Gitea Actions. Enable it under Site Administration → Configuration → Actions, then add a runner:
docker run -d --name gitea-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
-e GITEA_INSTANCE_URL=https://git.yourdomain.com \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token-from-admin-panel> \
gitea/act_runner:latest
The registration token is available under Site Administration → Actions → Runners. Once connected, workflows in .gitea/workflows/*.yml behave almost identically to GitHub Actions syntax — a useful migration path if you're moving projects over.
Common Pitfalls / Troubleshooting
- Certificate never issues. Same root cause as any Caddy setup — DNS hasn't propagated, or ports 80/443 aren't reachable from the internet. Verify externally with
dig git.yourdomain.com before troubleshooting further.
- SSH clone fails with "Permission denied." Usually means your public key wasn't added in the web UI, or your local
~/.ssh/config is missing the custom port entry. Test connectivity directly with ssh -p 222 git@git.yourdomain.com — you should get a Gitea welcome message, not a shell.
- Leaving self-registration open. As with any self-hosted auth-bearing service, this is the easiest security mistake to make and the most important to close immediately after setup.
- Database connection errors on first boot. Postgres can take a few seconds longer to initialize than Gitea expects on a cold start. Add a
depends_on with a healthcheck (as shown in the Docker Compose guide referenced below) if this happens consistently.
- Losing data on a volume reset. Both
gitea_data and db_data are named volumes — running docker compose down -v destroys them. Never use -v casually on this stack.
- Forgetting backups entirely. Your entire git history, issues, and wiki content live in these two volumes. Treat them exactly like any other critical data and include them in your regular backup rotation.
Wrapping Up
You now have a fully self-hosted Git server with HTTPS, SSH access, and optional CI — a real alternative to handing every private repository to a third party. The setup mirrors patterns used across other self-hosted services (Docker Compose, Caddy for TLS), so if you've built one of those already, this one should feel familiar.
From here, sensible next steps include adding the Gitea data volumes to your existing backup routine, setting up webhook notifications to a Slack or Discord channel on push, and exploring Gitea's built-in package registry if you want to self-host npm or Docker image packages alongside your code.
Further Reading
Hashtags
#selfhosted #gitea #gitserver #devops #docker #dockercompose #opensource #linux #sysadmin #ci_cd #homelab #webdev #infrastructureascode #techblog #cloudcomputing #dataprivacy #softwaredevelopment #versioncontrol #git #caddy #letsencrypt #postgresql #itadmin #smallteamtools #developertools #sysadmin101 #privacyfirst #continuousintegration #selfhostedgit #techtutorial