The following article is a primer on managing self-hosted apps. It covers everything from keeping the Shipyard (or any other app) up-to-date, secure, backed up, to other topics like auto-starting, monitoring, log management, web server configuration and using custom domains.
- Providing Assets
- Running Commands
- Healthchecks
- Logs and Performance
- Auto-Starting at Boot
- Updating
- Backing Up
- Scheduling
- SSL Certificates
- Authentication
- Managing with Compose
- Environmental Variables
- Setting Headers
- Remote Access
- Custom Domain
- Securing Containers
- Web Server Configuration
- Running a Modified App
- Building your Own Container
Although not essential, you will most likely want to provide several assets to your running app.
This is easy to do using Docker Volumes, which lets you share a file or directory between your host system, and the container. Volumes are specified in the Docker run command, or Docker compose file, using the --volume
or -v
flags. The value of which consists of the path to the file / directory on your host system, followed by the destination path within the container. Fields are separated by a colon (:
), and must be in the correct order. For example: -v ~/khulnasoft/my-local-conf.yml:/app/user-data/conf.yml
In Shipyard, commonly configured resources include:
./user-data/conf.yml
- Your main application config file./public/item-icons
- A directory containing your own icons. This allows for offline access, and better performance than fetching from a CDN- Also within
./public
you'll find standard website assets, includingfavicon.ico
,manifest.json
,robots.txt
, etc. There's no need to pass these in, but you can do so if you wish /src/styles/user-defined-themes.scss
- A stylesheet for applying custom CSS to your app. You can also write your own themes here.
If you're running an app in Docker, then commands will need to be passed to the container to be executed. This can be done by preceding each command with docker exec -it [container-id]
, where container ID can be found by running docker ps
. For example docker exec -it 26c156c467b4 yarn build
. You can also enter the container, with docker exec -it [container-id] /bin/ash
, and navigate around it with normal Linux commands.
Shipyard has several commands that can be used for various tasks, you can find a list of these either in the Developing Docs, or by looking at the package.json
. These can be used by running yarn [command-name]
.
Healthchecks are configured to periodically check that Shipyard is up and running correctly on the specified port. By default, the health script is called every 5 minutes, but this can be modified with the --health-interval
option. You can check the current container health with: docker inspect --format "{{json .State.Health }}" [container-id]
, and a summary of health status will show up under docker ps
. You can also manually request the current application status by running docker exec -it [container-id] yarn health-check
. You can disable healthchecks altogether by adding the --no-healthcheck
flag to your Docker run command.
To restart unhealthy containers automatically, check out Autoheal. This image watches for unhealthy containers, and automatically triggers a restart. (This is a stand in for Docker's --exit-on-unhealthy
that was proposed, but not merged). There's also Deunhealth, which is super light-weight, and doesn't require network access.
docker run -d \
--name autoheal \
--restart=always \
-e AUTOHEAL_CONTAINER_LABEL=all \
-v /var/run/docker.sock:/var/run/docker.sock \
willfarrell/autoheal
You can view logs for a given Docker container with docker logs [container-id]
, add the --follow
flag to stream the logs. For more info, see the Logging Documentation. There's also Dozzle, a useful tool, that provides a web interface where you can stream and query logs from all your running containers from a single web app.
You can check the resource usage for your running Docker containers with docker stats
or docker stats [container-id]
. For more info, see the Stats Documentation. There's also cAdvisor, a useful web app for viewing and analyzing resource usage and performance of all your running containers.
You can also view logs, resource usage and other info as well as manage your entire Docker workflow in third-party Docker management apps. For example Portainer an all-in-one open source management web UI for Docker and Kubernetes, or LazyDocker a terminal UI for Docker container management and monitoring.
Docker supports using Prometheus to collect logs, which can then be visualized using a platform like Grafana. For more info, see this guide. If you need to route your logs to a remote syslog, then consider using logspout. For enterprise-grade instances, there are managed services, that make monitoring container logs and metrics very easy, such as Sematext with Logagent.
You can use Docker's restart policies to instruct the container to start after a system reboot, or restart after a crash. Just add the --restart=always
flag to your Docker compose script or Docker run command. For more information, see the docs on Starting Containers Automatically.
For Podman, you can use systemd
to create a service that launches your container, the docs explains things further. A similar approach can be used with Docker, if you need to start containers after a reboot, but before any user interaction.
To restart the container after something within it has crashed, consider using docker-autoheal
by @willfarrell, a service that monitors and restarts unhealthy containers. For more info, see the Healthchecks section above.
Shipyard is under active development, so to take advantage of the latest features, you may need to update your instance every now and again.
- Pull latest image:
docker pull khulnasoft/shipyard:latest
- Kill off existing container
- Find container ID:
docker ps
- Stop container:
docker stop [container_id]
- Remove container:
docker rm [container_id]
- Find container ID:
- Spin up new container:
docker run [params] khulnasoft/shipyard
You can automate the above process using Watchtower. Watchtower will watch for new versions of a given image on Docker Hub, pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially.
To get started, spin up the watchtower container:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower
For more information, see the Watchtower Docs
Stop your current instance of Shipyard, then navigate into the source directory. Pull down the latest code, with git pull origin master
, then update dependencies with yarn
, rebuild with yarn build
, and start the server again with yarn start
.
You can make a backup of any running container really easily, using docker commit
and save it with docker export
, to do so:
- First find the container ID, you can do this with
docker container ls
- Now to create the snapshot, just run
docker commit -p [container-id] my-backup
- Finally, to save the backup locally, run
docker save -o ~/shipyard-backup.tar my-backup
- If you want to push this to a container registry, run
docker push my-backup:latest
Note that this will not include any data in docker volumes, and the process here is a bit different. Since these files exist on your host system, if you have an existing backup solution implemented, you can incorporate and volume files within that system.
offen/docker-volume-backup is a useful tool for periodic Docker volume backups, to any S3-compatible storage provider. It's run as a light-weight Docker container, and is easy to setup, and also supports GPG-encryption, email notification, and routing away older backups.
To get started, create a docker-compose similar to the example below, and then start the container. For more info, check out their documentation, which is very clear.
version: '3'
services:
backup:
image: offen/docker-volume-backup:latest
environment:
BACKUP_CRON_EXPRESSION: "0 * * * *"
BACKUP_PRUNING_PREFIX: backup-
BACKUP_RETENTION_DAYS: 7
AWS_BUCKET_NAME: backup-bucket
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
data:
It's worth noting that this process can also be done manually, using the following commands:
Backup:
docker run --rm -v some_volume:/volume -v /tmp:/backup alpine tar -cjf /backup/some_archive.tar.bz2 -C /volume ./
Restore:
docker run --rm -v some_volume:/volume -v /tmp:/backup alpine sh -c "rm -rf /volume/* /volume/..?* /volume/.[!.]* ; tar -C /volume/ -xjf /backup/some_archive.tar.bz2"
All configuration and dashboard settings are stored in your user-data/conf.yml
file. If you provide additional assets (like icons, fonts, themes, etc), these will also live in the user-data
directory. So to backup all Shipyard data, this is the only directory you need to backup.
Since Shipyard is open source, there shouldn't be any need to backup the main container.
Shipyard also has a built-in cloud backup feature, which is free for personal users, and will let you make and restore fully encrypted backups of your config directly through the UI. To learn more, see the Cloud Backup Docs
If you need to periodically schedule the running of a given command on Shipyard (or any other container), then a useful tool for doing so it ofelia. This runs as a Docker container and is really useful for things like backups, logging, updating, notifications, etc. Crons are specified using Go's crontab format, and a useful tool for visualizing this is crontab.guru. This can also be done natively with Alpine: docker run -it alpine ls /etc/periodic
.
I recommend combining this with healthchecks for easy monitoring of jobs, and failure notifications.
Enabling HTTPS with an SSL certificate is recommended, especially if you are hosting Shipyard anywhere other than your home. This will ensure that all traffic is encrypted in transit.
If you are using NGINX Proxy Manager, then SSL is supported out of the box. Once you've added your proxy host and web address, then set the scheme to HTTPS, then under the SSL Tab select "Request a new SSL certificate" and follow the on-screen instructions.
If you're hosting Shipyard behind Cloudflare, then they offer free and easy SSL- all you need to do is enable it under the SSL/TLS tab. Or if you are using shared hosting, you may find this tutorial helpful.
Let's Encrypt is a global Certificate Authority, providing free SSL/TLS Domain Validation certificates in order to enable secure HTTPS access to your website. They have good browser/ OS compatibility with their ISRG X1 and DST CA X3 root certificates, support Wildcard issuance done via ACMEv2 using the DNS-01 and have Multi-Perspective Validation. Let's Encrypt provide CertBot an easy app for generating and setting up an SSL certificate.
This process can be automated, using something like the Docker-NGINX-Auto-SSL Container to generate and renew certificates when needed.
If you're not so comfortable on the command line, then you can use a tool like SSL For Free or ZeroSSL to generate your cert. They also provide step-by-step setup instructions for most platforms.
Once you've generated your SSL cert, you'll need to pass it to Shipyard. This can be done by specifying the paths to your public and private keys using the SSL_PRIV_KEY_PATH
and SSL_PUB_KEY_PATH
environmental variables. Or if you're using Docker, then just pass public + private SSL keys in under /etc/ssl/certs/shipyard-pub.pem
and /etc/ssl/certs/shipyard-priv.key
respectively, e.g:
docker run -d \
-p 8080:8080 \
-v ~/my-private-key.key:/etc/ssl/certs/shipyard-priv.key:ro \
-v ~/my-public-key.pem:/etc/ssl/certs/shipyard-pub.pem:ro \
khulnasoft/shipyard:latest
By default the SSL port is 443
within a Docker container, or 4001
if running on bare metal, but you can override this with the SSL_PORT
environmental variable.
Once everything is setup, you can verify your site is secured using a tool like SSL Checker.
Shipyard natively supports secure authentication using KeyCloak. There is also a Simple Auth feature that doesn't require any additional setup. Usage instructions for both, as well as alternative auth methods, has now moved to the Authentication Docs page.
When you have a lot of containers, it quickly becomes hard to manage with docker run
commands. The solution to this is docker compose, a handy tool for defining all a containers run settings in a single YAML file, and then spinning up that container with a single short command - docker compose up
. A good example of which can be seen in @abhilesh's docker compose collection.
You can use Shipyard's default docker-compose.yml
file as a template, and modify it according to your needs.
An example Docker compose, using the default base image from DockerHub, might look something like this:
---
version: "3.8"
services:
shipyard:
container_name: Shipyard
image: khulnasoft/shipyard
volumes:
- /root/my-config.yml:/app/user-data/conf.yml
ports:
- 4000:8080
environment:
- BASE_URL=/my-dashboard
restart: unless-stopped
healthcheck:
test: ['CMD', 'node', '/app/services/healthcheck']
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s
With Docker, you can define environmental variables under the environment
section of your Docker compose file. Environmental variables are used to configure high-level settings, usually before the config file has been read. For a list of all supported env vars in Shipyard, see the developing docs, or the default .env
file.
A common use case, is to run Shipyard under a sub-page, instead of at the root of a URL (e.g. https://my-homelab.local/shipyard
instead of https://shipyard.my-homelab.local
). In this use-case, you'd specify the BASE_URL
variable in your compose file.
environment:
- BASE_URL=/shipyard
You can also do the same thing with the docker run command, using the --env
flag.
If you've got many environmental variables, you might find it useful to put them in a .env
file. Similarly, for Docker run you can use --env-file
if you'd like to pass in a file containing all your environmental variables.
Any external requests made to a different origin (app/ service under a different domain) will be blocked if the correct headers are not specified. This is known as Cross-Origin Resource Sharing (CORS) and is a security feature built into modern browsers.
If you see a CORS error in your console, this can be easily fixed by setting the correct headers. This is not a bug with Shipyard, so please don't raise it as a bug!
The following section briefly outlines how you can set headers for common web proxies/ servers. More info can be found in the documentation for the proxy that you are using, or in the MDN Docs.
These examples are using:
Access-Control-Allow-Origin
header, but depending on what type of content you are enabling, this will vary. For example, to allow a site to be loaded in an iframe (for the modal or workspace views) you would useX-Frame-Options
.- The domain root (
/
), if your're hosting from a sub-page, replace that with your path. - A wildcard (
*
), which would allow access from traffic on any domain, this is discouraged, and you should replace it with the URL where you are hosting Shipyard. Note that for requests that transport sensitive info, like credentials (e.g. Keycloak login), the wildcard is disallowed all together and will be blocked.
See Caddy
header
docs for more info.
headers / {
Access-Control-Allow-Origin *
}
See NGINX
ngx_http_headers_module
docs for more info.
location / {
add_header Access-Control-Allow-Origin *;
}
Note this can also be done through the UI, using NGINX Proxy Manager.
See Træfɪk CORS headers docs for more info.
labels:
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.testheader.headers.accesscontrolalloworiginlist=https://foo.bar.org,https://example.org"
- "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.testheader.headers.addvaryheader=true"
See HAProxy Rewrite Response Docs for more info.
/
http-response add-header Access-Control-Allow-Origin *
See Apache
mode_headers
docs for more info.
Header always set Access-Control-Allow-Origin "*"
See Squid
request_header_access
docs for more info.
request_header_access Authorization allow all
Using a VPN is one of the easiest ways to provide secure, full access to your local network from remote locations. WireGuard is a reasonably new open source VPN protocol, that was designed with ease of use, performance and security in mind. Unlike OpenVPN, it doesn't need to recreate the tunnel whenever connection is dropped, and it's also much easier to setup, using shared keys instead.
- Install Wireguard - See the Install Docs for download links + instructions
- On Debian-based systems, it's
sudo apt install wireguard
- On Debian-based systems, it's
- Generate a Private Key - Run
wg genkey
on the Wireguard server, and copy it to somewhere safe for later - Create Server Config - Open or create a file at
/etc/wireguard/wg0.conf
and under[Interface]
add the following (see example below):Address
- as a subnet of all desired IPsPrivateKey
- that you just generatedListenPort
- Default is51820
, but can be anything
- Get Client App - Download the WG client app for your platform (Linux, Windows, MacOS, Android or iOS are all supported)
- Create new Client Tunnel - On your client app, there should be an option to create a new tunnel, when doing so a client private key will be generated (but if not, use the
wg genkey
command again), and keep it somewhere safe. A public key will also be generated, and this will go in our saver config - Add Clients to Server Config - Head back to your
wg0.conf
file on the server, create a[Peer]
section, and populate the following infoAllowedIPs
- List of IP address inside the subnet, the client should have access toPublicKey
- The public key for the client you just generated
- Start the Server - You can now start the WG server, using:
wg-quick up wg0
on your server - Finish Client Setup - Head back to your client device, and edit the config file, leave the private key as is, and add the following fields:
PublicKey
- The public key of the serverAddress
- This should match theAllowedIPs
section on the servers config fileDNS
- The DNS server that'll be used when accessing the network through the VPNEndpoint
- The hostname or IP + Port where your WG server is running (you may need to forward this in your firewall's settings)
- Done - Your clients should now be able to connect to your WG server :) Depending on your networks firewall rules, you may need to port forward the address of your WG server
# Server file
[Interface]
# Which networks does my interface belong to? Notice: /24 and /64
Address = 10.5.0.1/24, 2001:470:xxxx:xxxx::1/64
PrivateKey = xxx
ListenPort = 51820
# Peer 1
[Peer]
PublicKey = xxx
# Which source IPs can I expect from that peer? Notice: /32 and /128
AllowedIps = 10.5.0.35/32, 2001:470:xxxx:xxxx::746f:786f/128
# Peer 2
[Peer]
PublicKey = xxx
# Which source IPs can I expect from that peer? This one has a LAN which can
# access hosts/jails without NAT.
# Peer 2 has a single IP address inside the VPN: it's 10.5.0.25/32
AllowedIps = 10.5.0.25/32,10.21.10.0/24,10.21.20.0/24,10.21.30.0/24,10.31.0.0/24,2001:470:xxxx:xxxx::ca:571e/128
[Interface]
# Which networks does my interface belong to? Notice: /24 and /64
Address = 10.5.0.35/24, 2001:470:xxxx:xxxx::746f:786f/64
PrivateKey = xxx
# Server
[Peer]
PublicKey = xxx
# I want to route everything through the server, both IPv4 and IPv6. All IPs are
# thus available through the Server, and I can expect packets from any IP to
# come from that peer.
AllowedIPs = 0.0.0.0/0, ::0/0
# Where is the server on the internet? This is a public address. The port
# (:51820) is the same as ListenPort in the [Interface] of the Server file above
Endpoint = 1.2.3.4:51820
# Usually, clients are behind NAT. to keep the connection running, keep alive.
PersistentKeepalive = 15
A useful tool for getting WG setup is Algo. It includes scripts and docs which cover almost all devices, platforms and clients, and has best practices implemented, and security features enabled. All of this is better explained in this blog post.
SSH (or Secure Shell) is a secure tunnel that allows you to connect to a remote host. Unlike the VPN methods, an SSH connection does not require an intermediary, and will not be affected by your IP changing. However it only allows you to access a single service at a time. SSH was really designed for terminal access, but because of the latter mentioned benefits it's useful to setup, as a fallback option.
Directly SSH'ing into your home, would require you to open a port (usually 22), which would be terrible for security, and is not recommended. However a reverse SSH connection is initiated from inside your network. Once the connection is established, the port is redirected, allowing you to use the established connection to SSH into your home network.
The issue you've probably spotted, is that most public, corporate, and institutional networks will block SSH connections. To overcome this, you'd have to establish a server outside of your homelab that your homelab's device could SSH into to establish the reverse SSH connection. You can then connect to that remote server (the mothership), which in turn connects to your home network.
Now all of this is starting to sound like quite a lot of work, but this is where services like remot3.it come in. They maintain the intermediary mothership server, and create the tunnel service for you. It's free for personal use, secure and easy. There are several similar services, such as RemoteIoT, or you could create your own on a cloud VPS (see this tutorial for more info on that).
Before getting started, you'll need to head over to Remote.it and create an account.
Then setup your local device:
- If you haven't already done so, you'll need to enable and configure SSH.
- This is out-of-scope of this article, but I've explained it in detail in this post.
- Download the Remote.it install script from their GitHub
curl -LkO https://raw.githubusercontent.com/remoteit/installer/master/scripts/auto-install.sh
- Make it executable, with
chmod +x ./auto-install.sh
, and then run it withsudo ./auto-install.sh
- Finally, configure your device, by running
sudo connectd_installer
and following the on-screen instructions
And when you're ready to connect to it:
- Login to app.remote.it, and select the name of your device
- You should see a list of running services, click SSH
- You'll then be presented with some SSH credentials that you can now use to securely connect to your home, via the Remote.it servers
Done :)
If you're running Shipyard on your local network, behind a firewall, but need to temporarily share it with someone external, this can be achieved quickly and securely using Ngrok. It's basically a super slick, encrypted TCP tunnel that provides an internet-accessible address that anyone use to access your local service, from anywhere.
To get started, Download and install Ngrok for your system, then just run ngrok http [port]
(replace the port with the http port where Shipyard is running, e.g. 8080). When using https, specify the full local url/ ip including the protocol.
Some Ngrok features require you to be authenticated, you can create a free account and generate a token in your dashboard, then run ngrok authtoken [token]
.
It's recommended to use authentication for any publicly accessible service. Shipyard has an Auth feature built in, but an even easier method it to use the -auth
switch. E.g. ngrok http -auth="username:password123" 8080
By default, your web app is assigned a randomly generated ngrok domain, but you can also use your own custom domain. Under the Domains Tab of your Ngrok dashboard, add your domain, and follow the CNAME instructions. You can now use your domain, with the -hostname
switch, e.g. ngrok http -region=us -hostname=shipyard.example.com 8080
. If you don't have your own domain name, you can instead use a custom sub-domain (e.g. khulnasoft-shipyard.ngrok.io
), using the -subdomain
switch.
To integrate this into your docker-compose, take a look at the gtriggiano/ngrok-tunnel container.
There's so much more you can do with Ngrok, such as exposing a directory as a file browser, using websockets, relaying requests, rewriting headers, inspecting traffic, TLS and TCP tunnels and lots more. All or which is explained in the Documentation.
It's worth noting that Ngrok isn't the only option here, other options include: FRP, Inlets, Local Tunnel, TailScale, etc. Check out Awesome Tunneling for a list of alternatives.
For locally running services, a domain can be set up directly in the DNS records. This method is really quick and easy, and doesn't require you to purchase an actual domain. Just update your networks DNS resolver, to point your desired URL to the local IP where Shipyard (or any other app) is running. For example, a line in your hosts file might look something like: 192.168.0.2 shipyard.homelab.local
.
If you're using Pi-Hole, a similar thing can be done in the /etc/dnsmasq.d/03-custom-dns.conf
file, add a line like: address=/shipyard.example.com/192.168.2.0
for each of your services.
If you're running OPNSense/ PfSense, then this can be done through the UI with Unbound, it's explained nicely in this article, by Dustin Casto.
If you're using NGINX, then you can use your own domain name, with a config similar to the below example.
upstream shipyard {
server 127.0.0.1:32400;
}
server {
listen 8080;
server_name shipyard.mydomain.com;
# Setup SSL
ssl_certificate /var/www/mydomain/sslcert.pem;
ssl_certificate_key /var/www/mydomain/sslkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_session_timeout 5m;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://shipyard;
proxy_redirect off;
proxy_buffering off;
proxy_set_header host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}
Similarly, a basic Caddyfile
might look like:
shipyard.example.com {
reverse_proxy / nginx:8080
}
For more info, this guide on Setting up Domains with NGINX Proxy Manager and CloudFlare may be useful.
- Keep Docker Up-To-Date
- Set Resource Quotas
- Don't Run as Root
- Specify a User
- Limit Capabilities
- Prevent new Privileges being Added
- Disable Inter-Container Communication
- Don't Expose the Docker Daemon Socket
- Use Read-Only Volumes
- Set the Logging Level
- Verify Image before Pulling
- Specify the Tag
- Container Security Scanning
- Registry Security
- Security Modules
To prevent known container escape vulnerabilities, which typically end in escalating to root/administrator privileges, patching Docker Engine and Docker Machine is crucial. For more info, see the Docker Installation Docs.
Docker enables you to limit resource consumption (CPU, memory, disk) on a per-container basis. This not only enhances system performance, but also prevents a compromised container from consuming a large amount of resources, in order to disrupt service or perform malicious activities. To learn more, see the Resource Constraints Docs
For example, to run Shipyard with max of 1GB ram, and max of 50% of 1 CP core:
docker run -d -p 8080:8080 --cpus=".5" --memory="1024m" khulnasoft/shipyard:latest
Running a container with admin privileges gives it more power than it needs, and can be abused. Shipyard does not need any root privileges, and Docker by default doesn't run containers as root, so providing you don't specifically type sudo, you should be all good here.
Note that if you're facing permission issues on Debian-based systems, you may need to add your user to the Docker group. First create the group: sudo groupadd docker
, then add your (non-root) user: sudo usermod −aG docker [my-username]
, finally newgrp docker
to refresh.
One of the best ways to prevent privilege escalation attacks, is to configure the container to use an unprivileged user. This also means that any files created by the container and mounted, will be owned by the specified user (and not root), which makes things much easier.
You can specify a user, using the --user
param, and should include the user ID (UID
), which can be found by running id -u
, and the and the group ID (GID
), using id -g
.
With Docker run, you specify it like:
docker run --user 1000:1000 -p 8080:8080 khulnasoft/shipyard
Of if you're using Docker-compose, you could use an environmental variable
version: "3.8"
services:
shipyard:
image: khulnasoft/shipyard
user: ${CURRENT_UID}
ports: [ 4000:8080 ]
And then to set the variable, and start the container, run: CURRENT_UID=$(id -u):$(id -g) docker-compose up
Docker containers run with a subset of Linux Kernal's Capabilities by default. It's good practice to drop privilege permissions that are not needed for any given container.
With Docker run, you can use the --cap-drop
flag to remove capabilities, you can also use --cap-drop=all
and then define just the required permissions using the --cap-add
option. For a list of available capabilities, see the Privilege Capabilities Docs.
Note that dropping privileges and capabilities on runtime is not fool-proof, and often any leftover privileges can be used to re-escalate, see POS36-C.
Here's an example using docker-compose, removing privileges that are not required for Shipyard to run:
version: "3.8"
services:
shipyard:
image: khulnasoft/shipyard
ports: [ 4000:8080 ]
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
- DAC_OVERRIDE
- NET_BIND_SERVICE
To prevent processes inside the container from getting additional privileges, pass in the --security-opt=no-new-privileges:true
option to the Docker run command (see docs).
Run Command:
docker run --security-opt=no-new-privileges:true -p 8080:8080 khulnasoft/shipyard
Docker Compose
security_opt:
- no-new-privileges:true
By default Docker containers can talk to each other (using docker0
bridged network). If you don't need this capability, then it should be disabled. This can be done with the --icc=false
in your run command. You can learn more about how to facilitate secure communication between containers in the Compose Networking docs.
Docker socket /var/run/docker.sock
is the UNIX socket that Docker is listening to. This is the primary entry point for the Docker API. The owner of this socket is root. Giving someone access to it is equivalent to giving unrestricted root access to your host.
You should not enable TCP Docker daemon socket (-H tcp://0.0.0.0:XXX
), as doing so exposes un-encrypted and unauthenticated direct access to the Docker daemon, and if the host is connected to the internet, the daemon on your computer can be used by anyone from the public internet- which is bad. If you need TCP, you should see the docs to understand how to do this more securely.
Similarly, never expose /var/run/docker.sock
to other containers as a volume, as it can be exploited.
You can specify that a specific volume should be read-only by appending :ro
to the -v
switch. For example, while running Shipyard, if we want our config to be writable, but keep all other assets protected, we would do:
docker run -d \
-p 8080:8080 \
-v ~/shipyard-conf.yml:/app/user-data/conf.yml \
-v ~/shipyard-icons:/app/public/item-icons:ro \
-v ~/shipyard-theme.scss:/app/src/styles/user-defined-themes.scss:ro \
khulnasoft/shipyard:latest
You can also prevent a container from writing any changes to volumes on your host's disk, using the --read-only
flag. Although, for Shipyard, you will not be able to write config changes to disk, when edited through the UI with this method. You could make this work, by specifying the config directory as a temp write location, with --tmpfs /app/user-data/conf.yml
- but that this will not write the volume back to your host.
Logging is important, as it enables you to review events in the future, and in the case of a compromise this will let get an idea of what may have happened. The default log level is INFO
, and this is also the recommendation, use --log-level info
to ensure this is set.
Only use trusted images, from verified/ official sources. If an app is open source, it is more likely to be safe, as anyone can verify the code. There are also tools available for scanning containers,
Unless otherwise configured, containers can communicate among each other, so running one bad image may lead to other areas of your setup being compromised. Docker images typically contain both original code, as well as up-stream packages, and even if that image has come from a trusted source, the up-stream packages it includes may not have.
Using fixed tags (as opposed to :latest
) will ensure immutability, meaning the base image will not change between builds. Note that for Shipyard, the app is being actively developed, new features, bug fixes and general improvements are merged each week, and if you use a fixed version you will not enjoy these benefits. So it's up to you weather you would prefer a stable and reproducible environment, or the latest features and enhancements.
It's helpful to be aware of any potential security issues in any of the Docker images you are using. You can run a quick scan using Snyk on any image to output known vulnerabilities using Docker scan, e.g: docker scan khulnasoft/shipyard:latest
.
A similar product is Trivy, which is free an open source. First install it (with your package manager), then to scan an image, just run: trivy image khulnasoft/shipyard:latest
For larger systems, RedHat Clair is an app for parsing image contents and reporting on any found vulnerabilities. You run it locally in a container, and configure it with YAML. It can be integrated with Red Hat Quay, to show results on a dashboard. Most of these use static analysis to find potential issues, and scan included packages for any known security vulnerabilities.
Although over-kill for most users, you could run your own registry locally which would give you ultimate control over all images, see the Deploying a Registry Docs for more info. Another option is Docker Trusted Registry, it's great for enterprise applications, it sits behind your firewall, running on a swarm managed by Docker Universal Control Plane, and lets you securely store and manage your Docker images, mitigating the risk of breaches from the internet.
Docker supports several modules that let you write your own security profiles.
AppArmoris a kernel module that proactively protects the operating system and applications from external or internal threats, by enabling you to restrict programs' capabilities with per-program profiles. You can specify either a security policy by name, or by file path with the apparmor
flag in docker run. Learn more about writing profiles, here.
Seccomp (Secure Computing Mode) is a sandboxing facility in the Linux kernel that acts like a firewall for system calls (syscalls). It uses Berkeley Packet Filter (BPF) rules to filter syscalls and control how they are handled. These filters can significantly limit a containers access to the Docker Host's Linux kernel - especially for simple containers/applications. It requires a Linux-based Docker host, with secomp enabled, and you can check for this by running docker info | grep seccomp
. A great resource for learning more about this is DockerLabs.
The following section only applies if you are not using Docker, and would like to use your own web server
Shipyard ships with a pre-configured Node.js server, in server.js
which serves up the contents of the ./dist
directory on a given port. You can start the server by running node server
. Note that the app must have been build (run yarn build
), and you need Node.js installed.
If you wish to run Shipyard from a sub page (e.g. example.com/shipyard
), then just set the BASE_URL
environmental variable to that page name (in this example, /shipyard
), before building the app, and the path to all assets will then resolve to the new path, instead of ./
.
However, since Shipyard is just a static web application, it can be served with whatever server you like. The following section outlines how you can configure a web server.
Note, that if you choose not to use server.js
to serve up the app, you will loose access to the following features:
- Loading page, while the app is building
- Writing config file to disk from the UI
- Website status indicators, and ping checks
Example Configs
Create a new file in /etc/nginx/sites-enabled/shipyard
server {
listen 8080;
listen [::]:8080;
root /var/www/shipyard/html;
index index.html;
server_name your-domain.com www.your-domain.com;
location / {
try_files $uri $uri/ =404;
}
}
To use HTML5 history mode (appConfig.routingMode: history
), replace the inside of the location block with: try_files $uri $uri/ /index.html;
.
Then upload the build contents of Shipyard's dist directory to that location.
For example: scp -r ./dist/* [username]@[server_ip]:/var/www/shipyard/html
Copy Shipyard's dist folder to your apache server, sudo cp -r ./shipyard/dist /var/www/html/shipyard
.
In your Apache config, /etc/apche2/apache2.conf
add:
<Directory /var/www/html>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Add a .htaccess
file within /var/www/html/shipyard/.htaccess
, and add:
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]
Then restart Apache, with sudo systemctl restart apache2
Caddy v2
try_files {path} /
Caddy v1
rewrite {
regexp .*
to {path} /
}
Create a file names firebase.json
, and populate it with something similar to:
{
"hosting": {
"public": "dist",
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
- Login to your WHM
- Open 'Feature Manager' on the left sidebar
- Under 'Manage feature list', click 'Edit'
- Find 'Application manager' in the list, enable it and hit 'Save'
- Log into your users cPanel account, and under 'Software' find 'Application Manager'
- Click 'Register Application', fill in the form using the path that Shipyard is located, and choose a domain, and hit 'Save'
- The application should now show up in the list, click 'Ensure dependencies', and move the toggle switch to 'Enabled'
- If you need to change the port, click 'Add environmental variable', give it the name 'PORT', choose a port number and press 'Save'.
- Shipyard should now be running at your selected path an on a given port
If you'd like to make any code changes to the app, and deploy your modified version, this section briefly explains how.
The first step is to fork the project on GitHub, and clone it to your local system. Next, install the dependencies (yarn
), and start the development server (yarn dev
) and visit localhost:8080
in your browser. You can then make changes to the codebase, and see the live app update in real-time. Once you've finished, running yarn build
will build the app for production, and output the assets into ./dist
which can then be deployed using a web server, CDN or the built-in Node server with yarn start
. For more info on all of this, take a look at the Developing Docs. To build your own Docker container from the modified app, see Building your Own Container
Similar to above, you'll first need to fork and clone Shipyard to your local system, and then install dependencies.
Then, either use Shipyard's default Dockerfile
as is, or modify it according to your needs.
To build and deploy locally, first build the app with: docker build -t shipyard .
, and then start the app with docker run -p 8080:8080 --name my-dashboard shipyard
. Or modify the docker-compose.yml
file, replacing image: khulnasoft/shipyard
with build: .
and run docker compose up
.
Your container should now be running, and will appear in the list when you run docker container ls –a
. If you'd like to enter the container, run docker exec -it [container-id] /bin/ash
.
You may wish to upload your image to a container registry for easier access. Note that if you choose to do this on a public registry, please name your container something other than just 'shipyard', to avoid confusion with the official image.
You can push your build image, by running: docker push ghcr.io/OWNER/IMAGE_NAME:latest
. You will first need to authenticate, this can be done by running echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
, where CR_PAT
is an environmental variable containing a token generated from your GitHub account. For more info, see the Container Registry Docs.