You can self-host Gitea with Docker on a Linux VPS and get your own private Git server with a web interface, SSH access and issue tracking. This guide runs Gitea in a container, maps the web port and a separate SSH port, then walks you through pushing your first repository.
Gitea is small enough to run comfortably on a modest VPS, and because everything lives in one data directory you can back it up by copying a folder.
Before you start
You need:
- A Linux VPS running Ubuntu 22.04/24.04, Debian 12 or AlmaLinux/Rocky 8–9, with root or sudo access.
- Docker and the Compose plugin installed. If they are not, follow Install Docker and Docker Compose on a Linux VPS first.
- A DNS record if you plan to use a domain name. For testing, the server IP is fine — this guide uses
203.0.113.10. - Ports
3000(web) and2222(Git over SSH) reachable from your workstation.
Warning: The host's own sshd already listens on port 22. Do not map Gitea's SSH port to 22:22 on the host, or you will break your own remote login. Use 2222 as shown below.
Prepare the host
Create a working directory and a place for Gitea's data. Keeping the data in a bind mount makes backups and migrations simple.
mkdir -p ~/gitea/data
cd ~/giteaOpen the two ports in the firewall. On Ubuntu and Debian with UFW:
sudo ufw allow 3000/tcp
sudo ufw allow 2222/tcp
sudo ufw statusOn AlmaLinux and Rocky with firewalld:
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reloadIf your VPS provider has an external firewall or security group, allow the same two ports there as well.
Self-host Gitea with Docker Compose
Create docker-compose.yml in ~/gitea:
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=203.0.113.10
- GITEA__server__SSH_DOMAIN=203.0.113.10
- GITEA__server__ROOT_URL=http://203.0.113.10:3000/
- GITEA__server__SSH_PORT=2222
- GITEA__server__SSH_LISTEN_PORT=22
- GITEA__service__DISABLE_REGISTRATION=false
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"A few notes on the settings:
GITEA__server__SSH_PORTis the port clients use in clone URLs. It must match the host port you published (2222), not the port inside the container.GITEA__server__SSH_LISTEN_PORT=22is where Gitea's built-in SSH server listens inside the container. Leave it at 22 and let Docker do the mapping.ROOT_URLandDOMAINmust match the address you actually open in the browser, otherwise clone URLs and links will point at the wrong place.DISABLE_REGISTRATIONis set tofalseso you can create the first account. Turn it back totrueafterwards — see the next section.
Tip: latest moves. Once things work, pin the image to a specific tag from the Gitea registry or Docker Hub and update deliberately. Check the vendor's release notes before upgrading, since major versions sometimes change configuration keys.
Start the container:
docker compose up -d
docker compose logs -f giteaWait for the log line that says the server is listening, then press Ctrl+C to stop following the log. The first start creates the SQLite database and the data directory structure.
Create your account and lock registration
Open http://203.0.113.10:3000 in a browser. The first page is the initial configuration screen; the values are already filled in from the environment variables. Scroll down and click Install Gitea.
You land on the registration page. Create the first user — this account becomes the administrator. Use a long password and a real email address, because password resets depend on it.
Then close registration so nobody else can sign up:
nano docker-compose.ymlChange the line to:
- GITEA__service__DISABLE_REGISTRATION=trueApply it:
docker compose up -dExisting accounts keep working; the registration form disappears.
Add an SSH key and push your first repository
Git over SSH is what most people want, so set up a key before creating repositories.
On your workstation, generate a key if you do not have one:
ssh-keygen -t ed25519 -C "deploy@example.com"Print the public half and copy it:
cat ~/.ssh/id_ed25519.pubIn Gitea, open your avatar → Settings → SSH / GPG Keys → Add Key, paste the public key and save.
Test the connection. Gitea's SSH endpoint does not give a shell, so expect a greeting rather than a prompt:
ssh -T -p 2222 git@203.0.113.10You should see something like Hi deploy! You've successfully authenticated, but Gitea does not provide shell access.
Now create a repository in the web UI: click + → New Repository, name it demo, and leave it empty (no README) so your first push has somewhere to go.
On your workstation:
mkdir demo && cd demo
git init -b main
echo "# demo" > README.md
git add README.md
git commit -m "First commit"
git remote add origin ssh://git@203.0.113.10:2222/deploy/demo.git
git push -u origin mainThe ssh:// URL form matters here — it is how Git knows to use port 2222 instead of the default 22. Refresh the repository page and your commit appears.
Put Gitea behind HTTPS
Running the web UI on plain HTTP is fine for a first test, but credentials and session cookies travel in clear text. The usual fix is a reverse proxy that terminates TLS and forwards to 127.0.0.1:3000.
Two straightforward options:
- Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps — a web UI for certificates and proxy hosts.
- Traefik in Docker: automatic SSL and routing by container labels — configuration lives in labels on the container.
Whichever you pick, update GITEA__server__ROOT_URL to the https:// address and restart the container, or clone URLs will keep pointing at the IP and port.
Check it works
Run through these checks:
docker compose ps
curl -I http://203.0.113.10:3000
ssh -T -p 2222 git@203.0.113.10docker compose psshows thegiteaservice as running.curl -Ireturns200 OKor a redirect to the login page.- The SSH test prints the authentication greeting with your username.
- In the web UI, the clone box on your repository shows an
ssh://git@203.0.113.10:2222/...URL.
If all four pass, your private Git server is live.
Troubleshooting
Permission denied (publickey)on port 2222 — the key you offered is not the one stored in Gitea, or it was added to a different account. Check withssh -v -T -p 2222 git@203.0.113.10and compare the fingerprint it offers with the key listed in your profile.Connection refusedon port 2222 or 3000 — the port is not published or the firewall blocks it. Confirm theports:block indocker-compose.yml, then re-checkufw statusorfirewall-cmd --list-portsand any provider-level firewall.- Clone URLs show
localhostor the wrong port —ROOT_URL,DOMAINandSSH_PORTdo not match reality. Fix the environment variables, rundocker compose up -d, and reload the page. - Container restarts with permission errors on
/data— the files are owned by a different UID thanUSER_UID/USER_GID. Runsudo chown -R 1000:1000 ~/gitea/dataand start the container again. database is lockedin the logs — SQLite struggles with concurrent writes. SwitchGITEA__database__DB_TYPEto a PostgreSQL or MySQL service in the same Compose file, or reduce parallel activity. See Docker Compose depends_on and healthchecks for starting the database before Gitea.
Warning: docker compose down -v removes named volumes. Because this setup uses a bind mount at ./data, your repositories survive, but always take a copy of ~/gitea/data before upgrades or major changes.
Next steps
- Back up and move data safely with Docker volumes vs bind mounts.
- If containers cannot reach each other, work through Troubleshooting container networking: bridge vs host mode.
- Harden your own admin access with SSH hardening and host key verification.
