How I setup Gogs Docker on ports 80 and 22

This is just how I set it up Gogs (running in Docker) at home to listen on ports 80 & 22 rather than the default 3000 & 22. I already had things setup a certain way so that’s why I did what I do below, so it might not suit everyone. (Note: there’s an update to this post since it was initially published).

First off, thanks to the creators of Gogs. So nice being able to have a mini GitHub of your own at home. :) Prior to discovering Gogs I just had Git repos hosted on my Synology but I am spoilt with GitHub now so it was great to discover Gogs. There is a Gogs fork called Gitea in case anyone’s intersted. Apparently it started because the Gogs developer went AWOL and no one else had rights so it was forked. Lovely quote from the Gitea page, by Khalil Gibran:

Your children are not your children. They are the sons and daughters of Life’s longing for itself. They come through you but not from you, and though they are with you yet they belong not to you.

Anyhoo, moving to business.

Here’s the official instructions for setting up a Gogs container. I followed that.

Within the container the Gogs config is at /data/gogs/conf/app.ini, which if you followed the instructions actually resides on a volume you created and so is safe from any container deletions. This bit here is where you configure the HTTP and SSH ports:

I set the SSH port to 22 and that works fine. Now port 22 of the container will have SSH listening for requests. It’s a bit confusing that START_SSH_SERVER is false and I am talking about SSH running; that’s because Gogs has its own SSH server or it can make use of the OpenSSH server that’s very conveniently package in the container. The latter listens on port 22 and you can configure it via /app/gogs/docker/sshd_config to change the port. I set the port to 22 in the ssh_config file.

My original plan was to run Gogs in a Macvlan network and so gogs.rakhesh.home above was actually mapping to a LAN IP assigned to the container. That’s why I wanted both ports 22 and 80 to be mapped and work as usual because I wasn’t expecting any conflicts with the host ports 22 and 80. This plan changed later on, but I thought I’d mention it here for completeness.

Initially I thought changing the HTTP port would be a simple case of changing the HTTP_PORT above to 80. That didn’t work coz the process couldn’t bind to port 80. I thought that might be because of a lack of capabilities so I tried running the container with the NET_BIND_SERVICE capability but it didn’t help either. Then I realized that within the container the s6 process manager starts Gogs as a non-root user and obviously that can’t bind to port 80! I didn’t want to go down the route of changing it to be a root user so I went another way.

At this point I decided to put Gogs behind my reverse proxy. I already had Caddy running on the host for other purposes so I assigned gogs.rakhesh.home to the IP Caddy was listening on and added an entry like this in Caddyfile:

Now any requests to port 80 of gogs.rakhesh.home would be reverse proxied to Gogs listening on port 3000. Easy peasy.

You’ll notice the IP I am reverse proxying to is a localhost one though. That’s because I decided to change things a bit. You see, reverse proxying port 80 via Caddy is fine but that now breaks SSH as the two are now listening on different IPs. If I go to gogs.rakhesh.home port 80 I hit the Caddy IP and that’s fine, while if I go to port 22 I hit the host SSH server and not that of Gogs. So now I have a broken SSH!

To workaround this I decided to use some iptables port forwarding. But first since I don’t care about the container IP any more, I decided to map the localhost ports to the container ports so I can refer to them consistently (like in Caddy above and also in what I will do below). To that end here’s my Docker Compose file:

I went with Docker Compose for Gogs as that’s what I use nowadays. As I posted earlier I realized Compose is super useful even for single containers as you can capture the config easily.

So what am I doing above? I have a user-defined bridge network called gogs_network (that’s just best practice) (I create it beforehand though via docker network create gogs_network as doing it via the compose file results in it adding a prefix, which I didn’t like). I attach the container to this network, it gets some IP, and I map ports 3000 and 3022 on to ports 3000 and 22 inside the container. This way I can now have Caddy reverse proxy to and all I need is a way to port forward requests to my gogs.rakhesh.home IPs port 22 (which is a different IP to my host) to forward to I can use iptables for this:

I need to modify the PREROUTING chain for connections from other machines to the host. And I need to modify the OUTPUT chain for when the host itself is trying to connect to Gogs. The rules are simple and similar; all they do is take any connections to port 22 of my gogs.rakhesh.home IP and NAT it over to

Because I am NATing to localhost I have to do the following in addition to the above rules:

This tells the kernel to allow routing to the 127/8 subnet and has a security implication. Since this is for my home environment and I don’t anything sensitive I am trying to hide via binding to localhost, I don’t mind the risk.

Once this is in place I can now access gogs.rakhesh.home on both port 80 and 22.

Just to recap the Gogs config file, note that I am setting the HTTP port as 3000 and SSH port as 22, but I am setting the external URL without any port numbers (as that goes to Gogs).

You have to save the iptables rules however you usually do them, and also add net.ipv4.conf.eth0.route_localnet=1 to /etc/sysctl.conf. For my part I made a shell script with these (I added the sysctl too here so I know they all go together)(iptables rules get flushed each reboot so whenever I remove this service and reboot the rules will go away too):

And made a systemd service file to run these at startup:

That’s it!

For what it’s worth I made a systemd service to start Gogs too automatically:

Hope that helps anyone!


I have since changed things a bit. Rather than exposing the 127/8 network to the outside world I decided to forward to the bridged IP of the container itself. 

Here’s my new Docker Compose file:

It’s similar to the old one except that 1) I don’t export any ports any more (as in I don’t map a port from the host IP to the container) and 2) I assign the container a static IP.

With a static IP assigned I now point Caddy to that for port 80:

And now my firewall rules are the following:

Again, mostly similar to what I was doing before except that 1) I now point to the container IP rather than the local host IP & port and 2) I added a 3rd rule to allow traffic to the container. This way any traffic to the host’s IP I have kept aside for Gogs to be NAT’d to the internal IP for SSH.

Notice I don’t have the sysctl any more. Don’t need it.

Lastly, I changed the shell script I had created for the firewall rules to also cleanup after itself:

(I changed the filename of the script as you’ll see in the unit file below). Finally, here’s the systemd unit file with a slight change to depend on the gogs.service and also incorporate the script changes above: