24 min read

How to Self-Host Gitea and Access Your Git Server from Anywhere

Run your own private Git server with Gitea on Docker Compose. Expose the web interface and SSH cloning from anywhere using two Localtonet tunnels.

Remote browser and Git client reaching a self-hosted Gitea server through separate HTTP and TCP tunnels.
Gitea remains on the Docker host while separate tunnels provide remote web and SSH access.
Gitea Β· Self-Hosted Git Β· Docker Compose Β· Localtonet Β· 2026

Run your own Git service locally, then publish its web and SSH endpoints safely

Gitea provides repository hosting, code review, issues, releases, webhooks, and related collaboration features on infrastructure you control. This tutorial deploys Gitea with Docker Compose, keeps its host ports bound to loopback, verifies the installation locally, and exposes two distinct services through Localtonet: an HTTP tunnel for the web interface and a TCP tunnel for Git over SSH. It also covers clone URL configuration, SSH host-key validation, repository testing, troubleshooting, backups, restoration, and controlled upgrades.

πŸ”’ Loopback-only local ports 🌐 Separate HTTP and TCP tunnels ⚑ End-to-end clone, push, and pull verification

Understand the deployment architecture

This deployment has three layers. Gitea runs in a Docker container, Docker publishes its web and SSH ports only on the host's loopback interface, and the Localtonet client connects those local listeners to separate public relay endpoints. The Localtonet client initiates an outbound connection, so this workflow does not require inbound router port forwarding, firewall changes, a VPN, or a public IP address.

Browser traffic and Git SSH traffic must use different tunnel types. The HTTP tunnel forwards a public HTTPS URL to 127.0.0.1:3000. The TCP tunnel forwards a public host and port to 127.0.0.1:2222, which Docker maps to port 22 inside the Gitea container.

Traffic Public endpoint Localtonet target Container destination
Gitea web interface and Git over HTTPS HTTPS URL assigned to the HTTP tunnel 127.0.0.1:3000 server:3000
Git over SSH Host and port assigned to the TCP tunnel 127.0.0.1:2222 server:22
Local administration No public endpoint required http://127.0.0.1:3000 server:3000
A created tunnel is not automatically a running tunnel

After creating each Localtonet tunnel, press Start. Public access works only while the selected client device is connected and the corresponding tunnel is running. Stopping the client, stopping a tunnel, or losing connectivity makes that public path unavailable.

Prerequisites and planning

This tutorial assumes a Linux host on which you can run Docker containers and issue commands with an account permitted to use Docker. Docker Desktop can provide an equivalent Compose workflow on supported desktop systems, but filesystem ownership and host commands differ. Consult the installation instructions for your operating system rather than copying an unverified package command.

🐳 Docker and Compose Install a supported Docker Engine or Docker Desktop release and the Docker Compose plugin. The commands below use the current docker compose form.
πŸ’Ύ Persistent storage Reserve enough disk space for repositories, attachments, packages, logs, backups, and future growth. Self-hosting does not remove physical storage limits.
πŸ‘€ Filesystem permissions The example runs Gitea with UID and GID 1000. The bind-mounted data directory must be writable by that identity, or you must substitute the correct IDs for your host.
πŸ”Œ Available local ports Host ports 3000 and 2222 must be free. They remain bound to 127.0.0.1 rather than every network interface.
πŸ”‘ Developer SSH key Each developer using Git over SSH needs a private key on their workstation and must register the corresponding public key in Gitea.
🌐 Localtonet device Create a Localtonet account, install the current client application through the documented path for your OS, and confirm that its device token appears connected.

Verify Docker and Compose

docker version
docker compose version
docker info

All three commands should complete successfully. If access to the Docker daemon is denied, correct your Docker installation or account permissions before continuing. Do not work around a permissions problem by making the Docker socket broadly writable.

Check for port conflicts

ss -lnt | grep -E ':(3000|2222)[[:space:]]' || true

No output normally means both ports are available. If another process is listening, identify it before changing anything:

sudo ss -lntp | grep -E ':(3000|2222)[[:space:]]'

Stop the conflicting service if appropriate, or choose different unused host ports. If you change a host port, make the same change in the Docker port mapping and the relevant Localtonet local target.

Choose a controlled Gitea version

Avoid an unpinned latest image for a persistent server. Select an exact Gitea image tag that you have reviewed and tested, record it in version control, and retain the previous image tag for rollback. The Compose example uses 1.24.6 as a fixed example baseline rather than claiming it is the newest release. Before deployment, confirm that the selected tag is supported for your host architecture and review the upgrade notes between your current and target versions.

Do not expose the installation wizard before configuration is complete

Keep Gitea local until you have installed it, created the administrator account, reviewed registration settings, and verified authentication. Publishing an unfinished setup wizard could allow another person to initialize the instance.

Install Gitea with Docker Compose

Docker Compose file and terminal starting a Gitea container with persistent storage.
Docker Compose defines the Gitea service, port mappings, and persistent data storage.

The base deployment uses SQLite to keep the tutorial focused and avoid embedding database credentials in Compose. SQLite can suit a personal installation or a modest workload, but capacity depends on repository activity, concurrent users, enabled features, host storage, and backup requirements. Evaluate PostgreSQL or MySQL separately if your workload requires an external database.

1

Create the project and data directories

Choose a durable location rather than a temporary directory. The following example uses the current user's home directory.

2

Prepare ownership for the container

Assign the bind-mounted data directory to UID and GID 1000, which match the values used in this example. Substitute the intended IDs if your deployment uses another service identity.

3

Create the Compose file

Publish both ports on loopback from the outset. The web listener uses host port 3000, while SSH uses host port 2222 to avoid conflicting with a host SSH daemon on port 22.

4

Start the container

Launch Gitea in the background, inspect its state, and follow the startup logs before opening the installer.

5

Verify the local HTTP service

Confirm that Gitea responds through the loopback listener. Do not proceed to public tunneling until this local check works.

Complete steps 1 and 2 with:

mkdir -p "$HOME/gitea/data"
cd "$HOME/gitea"
sudo chown -R 1000:1000 data

Create compose.yaml:

services:
  server:
    image: docker.gitea.com/gitea:1.24.6
    container_name: gitea
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
    restart: unless-stopped
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "127.0.0.1:2222:22"

Validate and start the deployment:

docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100 server

The container should be running without a repeating restart cycle. A service can be running before it is ready, so test the HTTP listener separately:

curl -I http://127.0.0.1:3000/

An HTTP response confirms that the local listener is reachable. If the request is refused, inspect docker compose ps and docker compose logs server before continuing.

Complete the Gitea setup and configure public URLs

Open http://127.0.0.1:3000 in a browser on the Docker host. If the host has no local browser, use a temporary, authenticated administrative access method that does not publish the unfinished installer broadly.

Complete the initial installer

1

Select SQLite3

Use SQLite3 for this Compose deployment and keep its database within the persistent /data mount.

2

Use temporary local server values

Until the tunnels are running, use localhost for the HTTP and SSH domains, http://localhost:3000/ as the root URL, and 2222 as the displayed SSH port.

3

Create the administrator account

Choose a unique administrator username and a strong password. Store recovery information through your normal credential-management process.

4

Install and sign in

Finish installation, sign in, and review whether public self-registration should remain enabled for your use case.

What the Gitea server settings mean

Setting Purpose in this deployment Final value
DOMAIN The public hostname used for the Gitea web service Hostname copied from the active HTTP tunnel URL
ROOT_URL The canonical external URL used for generated web links and redirects The complete public HTTPS URL, including a trailing slash
SSH_DOMAIN The hostname Gitea places in SSH clone URLs Host copied from the active TCP tunnel endpoint
SSH_PORT The public TCP port displayed in SSH clone URLs Port copied from the active TCP tunnel endpoint

These values describe the external addresses clients use. They do not change Docker's internal ports. Gitea still listens on port 3000 for HTTP and port 22 for SSH inside the container.

Do not rely on an assumed administration page to edit ROOT_URL. In this deployment, edit the persistent configuration file at ./data/gitea/conf/app.ini, then restart the container. Wait until both Localtonet endpoints have been assigned before applying the final values.

Publish the Gitea web interface with an HTTP tunnel

HTTP tunnel carrying remote browser traffic to the private Gitea web interface.
The HTTP tunnel forwards requests from a public endpoint to Gitea’s local web service.

Install the Localtonet client through the current download or installation path for your operating system, run it on the Gitea host, and authenticate it through the supported application flow. Do not paste a device token into shell history, source files, screenshots, or this Compose project.

1

Confirm the Localtonet device is connected

Open our dashboard and verify that the device AuthToken corresponding to the Gitea host shows as connected.

2

Open the HTTP tunnel configuration

Create an HTTP tunnel and choose the required Process Type: Random Sub Domain, Custom Sub Domain, or Custom Domain. These options publish the same local web content at a public HTTPS address.

3

Select the device and relay server

Select the connected device AuthToken and an available relay server from the current dashboard. Do not copy a server code from an old tutorial because available values can change.

4

Set the local HTTP target

Enter 127.0.0.1 as the local IP address and 3000 as the local port.

5

Create and start the tunnel

Create the tunnel, then press Start. Copy the exact HTTPS URL displayed for the running tunnel.

Keep the assigned URL as YOUR_HTTP_URL in the examples below. If the dashboard displays https://gitea.example.invalid, then the hostname portion is gitea.example.invalid. The example domain is intentionally nonfunctional and must not be copied literally.

Test the running tunnel from a device that is not relying on the Gitea host's loopback interface:

curl -I YOUR_HTTP_URL

The result should be an HTTP response from Gitea. A gateway error usually means the tunnel is running but the Localtonet client cannot reach 127.0.0.1:3000. Recheck the Docker container, local curl test, target address, and target port.

Publish Git over SSH with a TCP tunnel

Git SSH traffic reaching Gitea through a separate public TCP tunnel.
A TCP tunnel exposes Gitea’s SSH service independently from the web interface.

SSH is not HTTP traffic, so it requires a separate raw TCP tunnel. Localtonet forwards the TCP connection without replacing Gitea's SSH authentication. Developers still authenticate with public keys registered in Gitea.

1

Open the TCP tunnel configuration

Create a TCP tunnel for the Gitea SSH listener.

2

Select the connected device AuthToken

Choose the same connected Gitea host used by the HTTP tunnel, unless another connected device can legitimately reach the service.

3

Select an available relay server

Choose a currently available server from the dashboard rather than hardcoding a region or server code.

4

Set the local TCP target

Enter 127.0.0.1 as the local IP address and 2222 as the local port.

5

Create and start the tunnel

Create the configuration, press Start, and copy the exact public host and port shown for the active tunnel.

Record these values as YOUR_TCP_HOST and YOUR_TCP_PORT. Do not assume the port is in a particular range or that an endpoint remains unchanged under every plan or configuration. If the assigned endpoint changes later, update Gitea and developer remotes accordingly.

Apply the final public addresses to Gitea

Back up the configuration file before editing it:

cd "$HOME/gitea"
cp data/gitea/conf/app.ini data/gitea/conf/app.ini.before-public-urls

Open data/gitea/conf/app.ini, locate its existing [server] section, and set the following values using the endpoints copied from your running tunnels:

[server]
DOMAIN = YOUR_HTTP_HOST
ROOT_URL = https://YOUR_HTTP_HOST/
SSH_DOMAIN = YOUR_TCP_HOST
SSH_PORT = YOUR_TCP_PORT
Edit the existing section instead of creating duplicate settings

Preserve other generated [server]SSH_PORT; the container's internal SSH listener remains on port 22.

Restart Gitea and inspect the logs:

docker compose restart server
docker compose ps
docker compose logs --tail=100 server

Open the public HTTPS URL and inspect a repository's clone options after creating one. The HTTP URL and SSH clone URL should now contain the two active Localtonet endpoints rather than localhost.

Verify the complete Git workflow

Remote workflow for opening Gitea, cloning over SSH, committing, and pushing changes.
A browser check, SSH clone, commit, and push confirm both public access paths.

A successful home page request proves only that the web path works. A useful acceptance test also verifies account access, repository creation, SSH host identity, public-key authentication, clone, commit, push, and pull.

1

Create a private test repository

Sign in through the public HTTPS URL and create an empty private repository named tunnel-test. Do not initialize it with files for this command sequence.

2

Register a developer public key

In the user's Gitea settings, open the SSH and GPG key area, add an SSH key, and paste only the public key. Never upload or paste the private key.

3

Validate the SSH host key

Obtain the server's host-key fingerprint through a trusted administrative path and compare it with the fingerprint shown on the first remote SSH connection. Do not accept an unexpected key blindly.

4

Test SSH authentication

Connect as the Gitea SSH user through the public TCP host and port. Successful Git SSH authentication may report that shell access is unavailable, which is normal for a Git-only account.

5

Create and push the first commit

Initialize a local repository, add the SSH remote displayed by Gitea, commit a small file, and push it.

6

Clone and pull through the public endpoint

Clone into a second directory, confirm the committed file is present, then perform a pull to verify routine read access.

If the container uses the standard persistent SSH host-key location, an administrator can inspect the Ed25519 public-key fingerprint locally with:

docker compose exec server ssh-keygen -lf /data/ssh/ssh_host_ed25519_key.pub

If that file does not exist in your selected Gitea image, inspect the container configuration and logs to locate the active host public key. Do not substitute an unverified fingerprint.

From the developer workstation, test the public SSH endpoint:

ssh -p YOUR_TCP_PORT git@YOUR_TCP_HOST

Compare the first-connection fingerprint with the trusted value obtained from the server. If they match, accept it. If the endpoint later presents a different key without a planned server restore or key rotation, stop and investigate before removing the old known_hosts entry.

Create and push the test repository, substituting your username and active endpoint:

mkdir tunnel-test
cd tunnel-test
git init
printf '# Tunnel test\n' > README.md
git add README.md
git commit -m "Verify remote Gitea access"
git branch -M main
git remote add origin ssh://git@YOUR_TCP_HOST:YOUR_TCP_PORT/YOUR_USERNAME/tunnel-test.git
git push -u origin main

Verify a fresh clone and pull:

cd ..
git clone ssh://git@YOUR_TCP_HOST:YOUR_TCP_PORT/YOUR_USERNAME/tunnel-test.git tunnel-test-clone
cd tunnel-test-clone
cat README.md
git pull

Finally, refresh the repository through the public HTTPS interface. The commit, branch, and file should be visible. This confirms that the web and SSH paths refer to the same Gitea instance.

Security and access practices

Self-hosting gives you operational control, but it also makes you responsible for account policy, patching, backups, monitoring, storage capacity, and incident response. A tunnel changes network reachability; it does not replace Gitea authorization or eliminate the need to secure the host.

πŸ”’ Keep host ports on loopback Preserve 127.0.0.1:3000:3000 and 127.0.0.1:2222:22 when the Localtonet client runs on the same host.
πŸ‘₯ Control registration Review self-registration after creating intended accounts. Disable it when users should be provisioned only by an administrator.
πŸ”‘ Use managed credentials Require strong account passwords and protect SSH private keys with appropriate filesystem controls and passphrases where practical.
🧩 Grant least privilege Give users and automation only the repository, organization, and administrative permissions they require.
πŸ“¦ Patch deliberately Track Gitea and container security updates, but test upgrades and retain a rollback path instead of following an unpinned image.
πŸ’Ύ Test restoration A backup is not complete evidence of recoverability. Restore it into an isolated test deployment and verify repositories, accounts, configuration, and Git operations.

SSH and HTTPS can both provide appropriate Git authentication when configured correctly. SSH commonly uses public keys, while Git over HTTPS can use credentials or tokens supported by the server and client workflow. Do not treat one transport as categorically secure and the other as categorically insecure.

Backups, restoration, upgrades, and routine operations

Create a database-consistent Gitea dump

Copying or archiving ./data while Gitea is accepting writes can produce a backup whose repository data, configuration, and database represent different moments. For this SQLite deployment, schedule a maintenance window, stop the normal container, and run Gitea's dump command in a one-off container against the same persistent data.

cd "$HOME/gitea"
docker compose stop server
docker compose run --rm --no-deps --user git server \
  gitea dump -c /data/gitea/conf/app.ini --file /data/gitea-dump.zip
docker compose up -d
docker compose ps
docker compose logs --tail=50 server

Copy data/gitea-dump.zip to protected backup storage, record the Gitea image tag that produced it, and protect the archive as sensitive data. It can contain source code, account information, configuration, and secrets. Keep multiple generations according to your recovery requirements.

External databases require their own consistency plan

If you replace SQLite with PostgreSQL or MySQL, use the database engine's supported backup procedure and coordinate it with Gitea's repository and configuration backup. A generic archive of live database files is not a valid database backup.

Restore and test a backup

Gitea restoration is a deliberate process rather than simply placing a ZIP file beside the container. Use a clean, isolated host or project directory, deploy the same Gitea version that created the dump, extract the archive, restore the configuration, repositories, application data, and database into their expected persistent locations, and correct ownership for the container identity. If the restored system uses an external database, import its logical dump with that database engine's supported tools.

After restoring the files, start Gitea, inspect its logs, and regenerate repository hooks using the administration function supported by the restored Gitea version when required. Then verify administrator sign-in, repository browsing, clone, commit, push, and pull. Keep the test deployment isolated from production webhooks, email delivery, runners, and public tunnels so it cannot perform unintended external actions.

Exact archive contents and restoration commands can vary by Gitea version and database type. Before a real recovery, inspect the dump and follow the restore procedure for the exact version recorded with the backup. Do not discover these details for the first time during an outage.

Perform a controlled upgrade

1

Review the upgrade path

Read the release and upgrade notes between the installed version and the target version. Avoid skipping unsupported migration steps.

2

Create and test a backup

Produce a consistent dump and confirm that your restore procedure works before changing the image.

3

Record the current state

Record the old image tag, configuration, tunnel endpoints, container status, and recent logs.

4

Change to an exact target tag

Edit only the image tag in compose.yaml, pull it, and recreate the container during a maintenance window.

5

Run acceptance tests

Check logs, local HTTP, public HTTPS, SSH authentication, clone, push, and pull before declaring the upgrade complete.

docker compose pull server
docker compose up -d
docker compose ps
docker compose logs --tail=200 server
curl -I http://127.0.0.1:3000/

If an upgrade performs a database migration, changing the image tag back may not be sufficient for rollback. Restore the pre-upgrade data and database into an isolated or stopped deployment using the tested recovery procedure, then start the previous image. Never run old application code against a database that it may no longer understand.

Routine commands

# Check status
docker compose ps

# Follow Gitea logs
docker compose logs -f server

# Restart Gitea
docker compose restart server

# Stop Gitea
docker compose stop server

# Start or recreate Gitea
docker compose up -d

The Compose restart policy can restart the Gitea container when Docker starts, unless the container was intentionally stopped. Localtonet startup behavior depends on the supported client installation and configuration for your operating system. Do not assume an unverified service command. After a reboot, verify the client connection and confirm that both tunnels show as running.

If either public endpoint changes, update the corresponding Gitea setting in app.ini, restart Gitea, and update existing Git remotes where necessary:

git remote -v
git remote set-url origin ssh://git@NEW_TCP_HOST:NEW_TCP_PORT/YOUR_USERNAME/YOUR_REPOSITORY.git

Troubleshooting common failures

Symptom Likely area Checks and corrective action
Docker reports that a port is already allocated Host port conflict Use ss -lntp to identify the listener. Stop it or select a new host port, then update both Compose and the Localtonet target.
Gitea repeatedly restarts or reports permission denied Data-directory ownership Inspect container logs and confirm that ./data is writable by the configured UID and GID. Avoid broad world-writable permissions.
Database initialization fails Database configuration or storage For SQLite, check the writable data path and free disk space. For an external database, verify service readiness, hostname, port, credentials, database existence, and network membership.
Local HTTP request is refused Container or port mapping Run docker compose ps, inspect logs, and verify the mapping is 127.0.0.1:3000:3000.
Public URL shows a gateway error HTTP tunnel target Confirm local curl succeeds, the selected AuthToken is connected, the tunnel is started, and its target is 127.0.0.1:3000.
Public URL redirects to localhost or displays bad links ROOT_URL or DOMAIN Set the exact public HTTPS host and URL in the existing [server] section, include the trailing slash in ROOT_URL, and restart Gitea.
Gitea displays the wrong SSH clone address SSH_DOMAIN or SSH_PORT Copy the host and port from the active TCP tunnel, update app.ini, restart Gitea, and refresh the repository page.
SSH connection times out TCP tunnel or local SSH listener Confirm the TCP tunnel is started, the client is connected, and Docker publishes 127.0.0.1:2222:22. Check Gitea logs for SSH startup errors.
SSH says permission denied or rejects the key User key authentication Confirm the public key is registered to the correct Gitea user, the client is offering the matching private key, and the remote user is git.
SSH warns that the host key changed Endpoint reassignment, restore, or key change Do not remove the warning automatically. Obtain the current server fingerprint through a trusted administrative path and investigate why it changed.
Public access stopped after reboot Client or tunnel lifecycle Verify Gitea locally, confirm the Localtonet device is connected, and confirm that both the HTTP and TCP tunnels are running.

Frequently asked questions

Why does this deployment need two Localtonet tunnels?

The Gitea web interface uses HTTP, while Git over SSH uses raw TCP. The HTTP tunnel sends browser traffic to 127.0.0.1:3000. The TCP tunnel sends SSH traffic to 127.0.0.1:2222. One does not replace the other.

Why are the Docker ports bound to 127.0.0.1?

Loopback binding prevents Docker from publishing Gitea directly on every host network interface. Because the Localtonet client runs on the same host, it can reach the loopback listeners without making those ports available directly to the LAN or internet.

Is the Gitea server available whenever the container is running?

It remains available locally when the container and host are running. Public availability also requires the Localtonet client device to be connected and each required tunnel to be started. The HTTP and TCP paths have independent tunnel states.

Can I use PostgreSQL instead of SQLite?

Yes, Gitea supports external database deployments, but they add credential management, readiness, networking, backup, and restore requirements. Use secrets rather than literal weak passwords, wait for database readiness, and use the database engine's supported logical backup procedure.

Can I migrate an existing repository to Gitea?

A plain Git repository can be transferred by adding a Gitea remote and pushing its branches and tags. Gitea also provides migration workflows, but the metadata that can be imported depends on the source platform, its API, credentials, permissions, and the Gitea version. Test a representative repository before planning a large migration.

Does Gitea Actions support every GitHub Actions workflow?

Do not assume complete compatibility. Gitea Actions supports workflows using its own runner architecture and familiar workflow concepts, but action behavior, marketplace dependencies, event support, tokens, services, and platform-specific features can differ. Validate each workflow and operate runners as separate trusted execution infrastructure.

What should I do if a public tunnel address changes?

Copy the new active endpoint from the dashboard. Update DOMAIN and ROOT_URL if the HTTP endpoint changed, or SSH_DOMAIN and SSH_PORT if the TCP endpoint changed. Restart Gitea and update existing Git remotes. Custom-domain availability and exact plan behavior should be checked against the current dashboard and documentation.

Connect your self-hosted Gitea server with Localtonet

Deploy Gitea behind loopback-only ports, connect the host to our platform, and run separate HTTP and TCP tunnels for the web interface and Git SSH traffic. Keep the client connected and both tunnels running whenever your team needs public access.

Get Started Free β†’

Corrections & updates

Substantive changes approved by the Localtonet editorial team are listed transparently below.

Rebuild the article using only current approved Localtonet components, remove inline styles and obsolete lt-pros-cons, lt-divider, and lt-usecase structures, change the hero title to a nonduplicative supporting line, and retain a validated clickable guide card whose links match real section IDs. Add a prerequisites section covering a supported Docker host, Docker Compose, storage, permissions, required local ports, SSH keys, a Localtonet account and connected client, and the distinction between the HTTP and TCP tunnels. Replace unsupp

Localtonet is a secure multi-protocol tunneling and proxy platform designed to expose localhost, devices, private services, and AI agents to the public internet supporting HTTP/HTTPS tunnels, TCP/UDP forwarding, mobile proxy infrastructure, file server publishing, latency-optimized game connectivity, and developer-ready AI agent endpoint exposure from a single unified control plane.

support