Docker & DevOps

Self-host Gitea with Docker for your own private Git server

Run Gitea in Docker on a Linux VPS with its own SSH and HTTP ports, then push your first repository over SSH.

IntermediateLinux server7 min read

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) and 2222 (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.

Bash
mkdir -p ~/gitea/data
cd ~/gitea

Open the two ports in the firewall. On Ubuntu and Debian with UFW:

Bash
sudo ufw allow 3000/tcp
sudo ufw allow 2222/tcp
sudo ufw status

On AlmaLinux and Rocky with firewalld:

Bash
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload

If 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:

YAML
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_PORT is 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=22 is where Gitea's built-in SSH server listens inside the container. Leave it at 22 and let Docker do the mapping.
  • ROOT_URL and DOMAIN must match the address you actually open in the browser, otherwise clone URLs and links will point at the wrong place.
  • DISABLE_REGISTRATION is set to false so you can create the first account. Turn it back to true afterwards — 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:

Bash
docker compose up -d
docker compose logs -f gitea

Wait 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:

Bash
nano docker-compose.yml

Change the line to:

YAML
      - GITEA__service__DISABLE_REGISTRATION=true

Apply it:

Bash
docker compose up -d

Existing 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:

Bash
ssh-keygen -t ed25519 -C "deploy@example.com"

Print the public half and copy it:

Bash
cat ~/.ssh/id_ed25519.pub

In 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:

Bash
ssh -T -p 2222 git@203.0.113.10

You 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:

Bash
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 main

The 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:

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:

Bash
docker compose ps
curl -I http://203.0.113.10:3000
ssh -T -p 2222 git@203.0.113.10
  • docker compose ps shows the gitea service as running.
  • curl -I returns 200 OK or 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 with ssh -v -T -p 2222 git@203.0.113.10 and compare the fingerprint it offers with the key listed in your profile.
  • Connection refused on port 2222 or 3000 — the port is not published or the firewall blocks it. Confirm the ports: block in docker-compose.yml, then re-check ufw status or firewall-cmd --list-ports and any provider-level firewall.
  • Clone URLs show localhost or the wrong port — ROOT_URL, DOMAIN and SSH_PORT do not match reality. Fix the environment variables, run docker compose up -d, and reload the page.
  • Container restarts with permission errors on /data — the files are owned by a different UID than USER_UID/USER_GID. Run sudo chown -R 1000:1000 ~/gitea/data and start the container again.
  • database is locked in the logs — SQLite struggles with concurrent writes. Switch GITEA__database__DB_TYPE to 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.