Protecting self-hosted Coolify apps with Tailscale
Self-host apps securely with Coolify and Tailscale. This guide provides instructions for setup, deployment on a private Tailscale network, and public access for Next.js on Coolify.
Coolify is a self-hosting platform designed to simplify the deployment and management of containerized applications, databases, and other applications.
Cloud hosting platforms like Vercel and Netlify offer convenience. Coolify gives developers control and flexibility. The Coolify minimum server specs are 2 CPUs and 2GB memory which means the entry cost on most VM providers is only $10-$20/m - a pretty cheap way to host all your side projects.
So if you're looking to explore self-hosting, reduce long-term costs for certain applications, or simply want more control over your infrastructure, Coolify is worth playing around with.
However, it's important to note that self-hosting comes with added security responsibilities. In this guide I’ll show you how to protect your Coolify instance using Tailscale. Tailscale enables you to restrict access to a private network accessible only from your authorized devices, while still selectively exposing specific applications to the public internet.
I’ll use Next.js as an example because it’s what we use at Arcjet to build the dashboard for our developer security SDK. But this approach will work for any application.
Set up Tailscale
Before diving into Coolify's setup, I always prioritize security by installing Tailscale first. This approach allows us to establish a secure private network and limit access to your Coolify instance, minimizing the chance of compromise during the initial configuration.
Install Tailscale
Tailscale can be easily installed on your server. If you're using Ubuntu Linux (like we are in this guide), SSH into your machine and execute the following command:
curl -fsSL https://tailscale.com/install.sh | sh
Set up Tailscale with SSH
Enhance your SSH security by leveraging Tailscale SSH. This feature delegates authentication to your existing Tailscale identity provider (e.g., Google or GitHub), eliminating the need to manage separate Linux users and SSH keys.
Start Tailscale with the --ssh flag:
sudo tailscale up --ssh
Obtain your server’s Tailscale IP address:
tailscale ip
Now, you can SSH into your server securely over the Tailscale network from any device with the Tailscale client installed:
ssh ubuntu@IP
# e.g. ssh ubuntu@100.103.39.84
Harden Coolify instance
With Tailscale up and running, we'll utilize Ubuntu's built-in firewall (UFW) to lock down all public access to your server. This is recommended by Tailscale and ensures that your server is only reachable via your private Tailscale network.
Later in this guide we’ll open up a Next.js app publicly.
Configure UFW
Here we will enable UFW, deny all incoming traffic by default, then allow all traffic on the Tailscale private network:
# Enable UFW
sudo ufw enable
# Disable all incoming traffic by default
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow traffic from Tailscale
sudo ufw allow in on tailscale0
# Restart
sudo ufw reload
sudo service ssh restart
Verify that your server is no longer accessible via its public IP, but remains reachable through its Tailscale IP. You can check the status with:
sudo ufw status
Status: active
To Action From
-- ------ ----
Anywhere on tailscale0 ALLOW Anywhere
Anywhere (v6) on tailscale0 ALLOW Anywhere (v6)
With your server secured, you're now ready to proceed with the Coolify installation.
Install Coolify
With your server now protected behind Tailscale, it's time to install Coolify. Thankfully, Coolify provides a convenient installation script that simplifies the process.
SSH into your server (using its Tailscale IP) and run the following command:
The installation might take a few minutes, depending on your server's resources and network speed.
Before logging into Coolify for the first time we need to allow local Docker network access.
Allow local Docker network access
Our UFW firewall, while essential for security, is currently blocking all network traffic - including communication within local Docker networks that Coolify relies on. To ensure Coolify operation, we need to selectively open access within these networks.
1. Identify Coolify's Docker Networks
First, let's identify the Docker networks used by Coolify:
sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
f9f624e3952d bridge bridge local
4c09c9a40d16 coolify bridge local
396a298f37ee host host local
e6f3a7930e66 none null local
Look for networks with names bridge and coolify
2. Inspect Network Subnets
Inspect the relevant networks to determine their subnet configurations:
Within the output, locate the Subnet field under IPAM -> Config. You'll see something like 172.17.0.0/16 or 172.18.0.0/16
3. Allow Local Network Traffic
Now, let's update UFW to allow traffic within these subnets:
sudo ufw allow from 172.18.0.0/16 to 172.17.0.1
sudo ufw allow from 172.17.0.0/16 to 172.17.0.1
These commands allow communication from containers within the specified subnets to the default Docker host (172.17.0.1). Which will give us this final set of UFW rules:
sudo ufw status
Status: active
To Action From
-- ------ ----
Anywhere on tailscale0 ALLOW Anywhere
172.17.0.1 ALLOW 172.22.0.0/16
172.17.0.1 ALLOW 172.18.0.0/16
Anywhere (v6) on tailscale0 ALLOW Anywhere (v6)
Complete Coolify setup
Load the Coolify setup web UI from http://your_server_tailscale_ip:8000 and follow the instructions to register an account. The onboarding wizard will ask you to create the default configuration with a localhost server.
If you have skipped this process or you get an error you can manually create a server after existing the onboarding with the IP Address/Domain of host.docker.internal
Deploy Next.js to Coolify
Coolify excels at deploying any containerized application. For this guide, we'll deploy the Arcjet Example Next.js application to illustrate the process. If you already have a Next.js project, you can easily adapt the steps.
1. Create a New Project
In the Coolify dashboard, click "Projects" -> "Add" and give your project a meaningful name.
Create a new environment or select an existing one.
2. Add a Public Repository Resource
Click "Add a new resource" -> "Public Repository." (Coolify also supports Dockerfiles, databases, and other resource types.)
Select the server you created earlier (typically labeled "localhost") and choose "Standalone Docker" as the destination.
Enter the repository URL of your Next.js project and click "Check repository." For demonstration purposes, we'll use the Arcjet example application: https://github.com/arcjet/arcjet-js-example
Leave the default build options (Nixpacks, Port 3000, Static Site Unticked) and click "Continue."
3. Configure Network Settings
By default, Coolify assigns a unique domain based on your server's public IP to your app (e.g., http://q84ko8w.44.153.23.63.sslip.io). However, since we've restricted access to the Tailscale network, this won't work. Let's configure Coolify to make your Next.js app accessible within your private network.
In the "Ports Exposed" field, ensure 3000 is listed.
In the "Ports Mappings" field, enter 3000:3000. This maps the internal container port 3000 to the external port 3000.
4. Set Environment Variables (Optional)
If your Next.js app requires any environment variables, add them in this section. For the Arcjet example, you'd add an ARCJET_KEY variable. You can get a key for free by signing up to Arcjet.
5. Deploy
Click "Save" and then "Deploy." Coolify will build and deploy your Next.js application. The build process may take a few minutes, depending on your server's specifications and the size of your project.
Accessing Next.js on Coolify on a private network
After deployment, you can access your Next.js app by navigating to: http://your_server_tailscale_ip:3000
At this point, your app is only accessible within your private Tailscale network. In the following section, we'll guide you through exposing your Next.js application publicly using Tailscale Funnel, granting access to users outside your private network.
Public Next.js on Coolify
With your Next.js app running privately on your Tailscale network, let's now open it up to the world using Tailscale Funnel.
sudo tailscale funnel 3000
You may be prompted to adjust settings in your Tailscale account to enable Funnel.
Tailscale will then provide you with a unique public URL for your app. Anyone with the public URL can now access your Next.js app securely over HTTPS, even if they are not part of your Tailscale network.
Other security considerations
Now we have the server secured from the network, we may also want to secure the application itself.
While Tailscale Funnel simplifies public exposure, it's crucial to maintain good security practices for your Next.js app. This includes:
Input Validation: Ensure proper input validation to prevent vulnerabilities like cross-site scripting (XSS) and SQL injection. Use a package like zod or Valibot (or others) to validate data.
Authentication/Authorization: Implement appropriate authentication and authorization mechanisms to protect sensitive data and restrict access as needed. Consider Clerk (which offers edge-compatible middleware) or NextAuth (soon to be Auth.js)
Regular Updates: Keep your Next.js app and its dependencies up-to-date to address security patches and vulnerabilities.
You can learn more about how to secure your Next.js application in our Next.js security checklist. Arcjet can also help with rate limiting, bot detection, email validation, and attack protection.
Why did we do this?
Why opt for this setup with Coolify and Tailscale, especially when Coolify offers built-in features for public access and custom domains.
Here's why our approach prioritizes security:
Defense in Depth: Coolify exposes containers publicly and runs as root by default. A remote code execution vulnerability could lead to server compromise if it was directly accessible on the internet. Our approach adds an extra layer of protection by placing your Coolify instance behind a private Tailscale network. This reduces the attack surface and shields your server from direct exposure to the internet.
Deliberate Exposure: With Tailscale Funnel, you make a conscious choice about which services to expose publicly. This minimizes the risk of misconfigurations or accidental exposure, ensuring that only the intended applications are accessible from the internet.
Granular Control: You can easily adjust access controls and permissions within Tailscale, tailoring the level of exposure to your specific needs.
Network Flexibility: Tailscale allows you to connect to your Coolify instance from any device within your private network, regardless of its physical location.
Ultimately, this approach strikes a balance between functionality and security. It allows you to leverage the convenience of Coolify while minimizing the risks associated with self-hosting. By prioritizing security from the outset, you lay a solid foundation for a resilient and secure self-hosting environment.
Nosecone is an open source library to set security headers like Content Security Policy (CSP) and HTTP Strict Transport Security (HSTS) on Next.js, SvelteKit, and other JavaScript frameworks using Bun, Deno, or Node.js. Security headers as code.
Server actions are an elegant way to handle simple functions for common actions like form submissions, but they're a public API so you still need to consider security.