LXC, or Linux Containers, is a system built upon a number of features included in modern Linux kernels that allows for creation and management of many virtualized Linux systems on a single parent host system. However, unlike some virtualization solutions, no hardware emulation is used and the container shares the kernel with the host, which makes LXC incredibly lightweight and easy to get started with. Also, due to its reliance only on mainline kernel features, LXC can be used within the KVM images on DigitalOcean.
Some LXC's key benefits include:
Security - By running key services within separate containers, you can ensure that a security flaw in one will remain better isolated from affecting other services. This may sound very similar to chroot jails, but LXC offers much more than just file-system isolation.
Portability - LXC containers can be zipped up and moved to any other host with the same processor architecture. This works great when your scaling your application, as individual services can be duplicated or moved with ease.
Limits - Due to its use of Linux cgroups, LXC containers can be configured with limitations on resources. When running a large number of containers, this can ensure that the most important ones get first priority.
Note: The following tutorial expects that all commands on the host be executed as the root user.
Warning: Do not attempt to use the following tutorial on a Debian system. Despite their similarities, the Debian LXC packages have a number of flaws that prevent proper isolation between containers and their host system.
Before we can create our first LXC containers, we need to install the tools needed to manage and create them:
apt-get install lxc
This package installs of of LXC's requirements and also sets up the network structure for the containers. Once the package installation is complete, we can check the kernel and the configuration is ready by running lxc-checkconfig:
$ lxc-checkconfig Kernel configuration not found at /proc/config.gz; searching... Kernel configuration found at /boot/config-3.8.0-19-generic --- Namespaces --- Namespaces: enabled Utsname namespace: enabled Ipc namespace: enabled Pid namespace: enabled User namespace: missing Network namespace: enabled Multiple /dev/pts instances: enabled --- Control groups --- Cgroup: enabled Cgroup clone_children flag: enabled Cgroup device: enabled Cgroup sched: enabled Cgroup cpu account: enabled Cgroup memory controller: enabled Cgroup cpuset: enabled --- Misc --- Veth pair device: enabled Macvlan: enabled Vlan: enabled File capabilities: enabled Note : Before booting a new kernel, you can check its configuration usage : CONFIG=/path/to/config /usr/bin/lxc-checkconfig
You may notice that 'User namespace' is indicated as missing; ideally it should be enabled, but LXC will still function without it for the use cases proposed in this tutorial.
New LXC containers are created with the lxc-create command. We'll use the ubuntu template to create and populate a new container named 'test-container':
lxc-create -n test-container -t ubuntu
This command may take a few minutes to complete the first time, as it downloads and caches all of the required components. However, it'll eventually return and you'll have a very minimal Ubuntu install created with debootstrap in a new container.
The last few lines of the creation output will also tell us the default username and password for the created container:
## # The default user is 'ubuntu' with password 'ubuntu'! # Use the 'sudo' command to run tasks as root in the container. ##
At any time, we can check the status of our containers with lxc-ls --fancy, which will currently show our container in a stopped state:
# lxc-ls --fancy NAME STATE IPV4 IPV6 AUTOSTART ---------------------------------------------- test-container STOPPED - - NO
Once the config file is saved, the next step is to get our container running, which can be achieved with lxc-start:
lxc-start -n test-container -d
This command will return immediately, whilst the container will start in the background. After a few seconds, try lxc-ls --fancy again and we'll see the container is running and has been assigned an IP address:
# lxc-ls --fancy NAME STATE IPV4 IPV6 AUTOSTART ---------------------------------------------------- test-container RUNNING 10.0.3.143 - NO
Note that the assigned IP belongs to an internal bridge, meaning that the containers are not accessible from your external DigitalOcean IP but do have access to the internet. In the next section, we'll look at port-forwarding to allow services within the containers to listen on the public VPS interface.
Finally, we can access our container with lxc-console:
lxc-console -n test-container
This will enter you into a regular login prompt running for the container, where you can use the username/password combo given during the creation of your VPS (ubuntu/ubuntu). It is recommended though that you change this default password for any containers that will host internet facing services.
Once logged in, you have a regular bash prompt from which you can do almost anything you would on the host machine. As this is such a minimal install, you may notice that some of the tools you use are absent, but these can be installed through apt-get if required.
When you are done exploring your new container, you can exit its console and return to the host by typing Ctrl-A followed by Q.
As an example, we'll install nginx in our container and make it accessible from the internet. Our container is effectively running its own Ubuntu VPS instance, meaning installing a service is much the same as installing on the host itself; therefore many of the tutorials here on DigitalOcean should work great even in the container.
Once you are connected through lxc-console, we can install the nginx package:
sudo apt-get install nginx sudo service nginx start
This will install nginx and ensure that is is running, but we cannot yet access it from the host's public interface. You can see this is the case if you navigate to http://your-DO-ip and note that it'll not yet be responding.
As mentioned earlier, the containers are given IPs in a private range which operates behind a NAT and are hence normally inaccessible from the internet. This is similar to how most home networks operate, whereby all computers share one IP on the gateway.
Using iptables on the host it is possible to see how the NAT is configured:
# iptables -t nat -L Chain PREROUTING (policy ACCEPT) target prot opt source destination Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 10.0.3.0/24 !10.0.3.0/24
This setup allows all of the containers access to the external network but port-forwarding is required to expose services on the containers to the internet.
Looking again at lxc-ls --fancy output from above, we can see that our container was assigned the private IP 10.0.3.143 (this'll likely be different for you). By adding an iptables rule to the prerouting table, we can forward packets on port 80 to our nginx container's port 80.
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to 10.0.3.143:80
Note: If you already have a service on the host listening on port 80, this rule will forward all packets to the container (even when not running) and hence the host's service will be inaccessible.
Navigating to http://your-DO-ip should now show the default nginx page, telling us that the setup and port-forwarding has been successful:
This process can now be repeated in new containers for any other service you wish to run. The benefit of this method is that if a security flaw is found and exploited in nginx, the attacker would be unable to affect any other services, such as your database, without first finding another exploit to escape the container.
Besides just isolation, another massive benefit of using LXC is its ability to apply cgroup limits to the processes within a container.
Limits for a container are defined in its config file, which for our container can be found at /var/lib/lxc/test-container/config.
Memory limits can be used to set a maximum RAM usage for container. In this case, we'll limit our container to 50MB of memory:
lxc.cgroup.memory.limit_in_bytes = 50000000
CPU limits are defined slightly differently; unlike with memory, where physical limits are defined, CPU limits operate with CPU 'shares':
lxc.cgroup.cpu.shares = 100
These shares are not linked to any physical quantity but instead just represent relative allocations of CPU resources, meaning a container with more shares gets higher CPU access priority. The numbers used are completely arbitrary though, so giving one container 10 and another 20 is the same as giving them 1000 and 2000 respectively, as all it tells us is that the second container has twice the CPU share priority. Just ensure you are consistent with your scale between containers.
Once you've changed the cgroup limits in the config file, you'll need to shutdown and restart the container for the changes to take effect.
Alternatively, limits can be set temporarily on a running container with the lxc-cgroup command:
lxc-cgroup -n test-container cpu.shares 100
It is often the case that you'll want the containers to autostart after a reboot, particularly if they are hosting services. By default, containers will not be started after a reboot, even if they were running prior to the shutdown.
To make a container autostart, you simply need to symlink its config file into the /etc/lxc/auto directory:
ln -s /var/lib/lxc/test-container/config /etc/lxc/auto/test-container.conf
Now running lxc-ls --fancy again will show that our container is setup to autostart:
# lxc-ls --fancy NAME STATE IPV4 IPV6 AUTOSTART ---------------------------------------------------- test-container RUNNING 10.0.3.143 - YES
Once you have finished with a container, it can be removed from your system to free up disk space. First, ensure the container is stopped with lxc-shutdown:
lxc-shutdown -n test-container
This will attempt to safely shutdown the container. Replacing lxc-shutdown with lxc-stop will perform an instant termination of the container, which is analogous to pulling the power plug on a real machine and may leave data in a corrupt state. Hence, use lxc-stop only in cases where lxc-shutdown fails.
Once stopped and you are sure there is no data you wish to retain on the container, it can be destroyed with lxc-destroy:
lxc-destroy -n test-container
This has shown the basic life-cycle of LXC containers and how they might benefit your setup. If you need any further advice or assistance, leave a comment below.
If you’ve enjoyed this tutorial and our broader community, consider checking out our DigitalOcean products which can also help you achieve your development goals.